在前兩天的時候有提過,盡量使用composition,而不是繼承,但如果真得需要使用繼承,需要做哪些事呢?
需要說明可以覆寫的方法(public
或protected
的方法)和constructor,會呼叫哪些方法和回傳的結果,以及結果會對caller造成什麼影響。寫出會呼叫哪些方法,可以在該方法有異動的時候,可以確認行為是不是有變動。例如前天的範例,HashSet
的addAll
應該在文件告知有呼叫add
。
但是如果提供太少可覆寫的方法,又會失去繼承的意義,所以需要明智地選擇要提供哪些protected
的方法,既可提供繼承,又不會像public
的方法被過多類別使用,影響範圍過大。
以java.util.AbstractList
的protected void removeRange(int fromIndex, int toIndex)
為例,提供子類別可以改寫這個方法,更彈性的移除list的item,不必花費時間設計如何使用clear()
移除sublist。
並使用繼承它的子類別,來測試繼承之後的行為。
因為子類別被實體化的時候,會先呼叫父類別的constructor,如果被覆寫的方法有使用只有子類別才有的欄位或方法,會出現不預期的行為。
例如下面的範例,子類別的doSomething()
用了父類別沒有的欄位,結果Child()
實體化的過程中,先呼叫Parent()
的constructor,Parent()
呼叫了doSomething()
,但doSomething()
已經被子類別覆寫,而且在還沒呼叫子類別constructor的情況下,存取了父類別沒有的欄位childField
,因為childField
還沒有被給值,結果就拿到null
。
class Parent {
public Parent() {
// Constructor of Parent class
System.out.println("Parent constructor called");
doSomething();
}
protected void doSomething() {
System.out.println("Parent doSomething called");
}
}
public class Child extends Parent {
private String childField;
public Child() {
// Constructor of Child class
this.childField = "Child specific field";
System.out.println("Child constructor called");
}
@Override
protected void doSomething() {
System.out.println("Child doSomething called");
System.out.println("Child field value: " + childField); // Potential problem
}
public static void main(String[] args) {
Child child = new Child();
child.doSomething(); // Child doSomething called
}
}
上面的範例,很好的示範實體化時,若父類別的方法比子類別更早被呼叫,且使用了被覆寫的方法,出現的不預期行為,所以實作Cloneable
和Serializable
的類別需特別注意,如果實作這兩個interface的類別有子類別,子類別有覆寫clone
和readObject
,要小心有沒有使用父類別沒有的欄位和方法。
從上面的說明可以知道,設計一個可以被繼承的類別,必須做很多事確保子類別不會濫用這個功能,所以類別如果沒有很好的說明文件,還是不要輕易被繼承,可以把constructor設為private
,改用static factories,就可以防止被繼承,並改用前兩天介紹的composition去實現擴增功能的需求。